﻿using System;
using System.Collections.Generic;
using System.Linq;

namespace SoftSource.Reactive
{
    /// <summary>
    /// The goal for ObservableQueue is to be able to pass it an IObservable, 
    /// and have the subscribe operations be queued until it is time to start subscribing to the IObservable.
    /// 
    /// If there is an operation that can only be executed one at a time, 
    /// but it needs to appear as if they are all executing at once, this queue will allow buffering the subscribers without blocking.
    /// 
    /// A sample operation is saving objects in a tracked collection. 
    /// Assume that each save operation is a single IObservable that will complete in a finite amount of time.
    /// And I cannot attempt to save several change set at once, but I want it to appear as if I am. 
    /// By buffering each save operation, we can simulate each call as executing immediately.
    /// </summary>
    /// <typeparam name="T">The type of IObservable to queue</typeparam>
    public sealed class ObservableQueue<T>
    {
        private Queue<Action> queue;
        private bool done;

        public ObservableQueue()
        {
            this.queue = new Queue<Action>();
            this.done = true;
        }

        /// <summary>
        /// Queues the IObservable to only begin executing once all previous IObservables have completed.
        /// </summary>
        /// <param name="observable">The IObservable to queue</param>
        /// <returns></returns>
        public IObservable<T> QueueObservable(IObservable<T> observable)
        {
            // Wrap the observable so that it will execute the next operation when it has completed.
            var completionObservable = this.GetCompletionObservable(observable);

            // Defer subscriptions to the observable
            var deferred = completionObservable.Defer();

            // Queue the Set operation for later. 
            // This will be executed by the observable that just completed.
            this.Enqueue(deferred.Set);

            return deferred;
        }

        private IObservable<T> GetCompletionObservable(IObservable<T> observable)
        {
            return observable.Finally(() => // Only when the observable is completed (either by error or valid completion)
                {
                    lock (this.queue)
                    {
                        if (this.queue.Count > 0) // If there is something in the queue
                        {
                            this.ExecuteNext(); // Allow the next Observable to accept subscribers
                        }
                        else
                        {
                            this.done = true; // If this was the last one in the queue, then we are done
                        }
                    }
                });
        }

        private void ExecuteNext()
        {
            lock (this.queue)
            {
                Action action = this.queue.Dequeue(); // Get the next Observable executor out of the queue

                if (action != null)
                {
                    this.done = false; // We are about to begin accepting subscribers, so flag us as 'Not done'
                    action(); // Start accepting subscribers                
                }
            }
        }

        private void Enqueue(Action action)
        {
            lock (this.queue)
            {
                this.queue.Enqueue(action);
                
                if (this.done)
                {
                    // At this point there is only one item in the queue. 
                    // That means we can start executing immediately.
                    this.ExecuteNext(); 
                }
            }
        }
    }
}
